﻿//------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
//------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Microsoft.ResourceManagement.Samples.ResourceManagementClient.ResourceManagement;
using Microsoft.ResourceManagement.Samples.ResourceManagementClient.WSTransfer;
using Microsoft.ResourceManagement.Samples.ResourceManagementClient.Utilities;
using Microsoft.ResourceManagement.Samples.ResourceManagementClient.ExtensionIdentityManagementOperations;
using System.Text;
using System.Globalization;
using System.Xml;
using System.ServiceModel;
using System.ServiceModel.Channels;

namespace Microsoft.ResourceManagement.Samples.ResourceManagementClient
{

    /// <summary>
    /// This class abstracts the WS-Transfer interface for the Resource Management Service.
    /// </summary>
    public class WSTransferClient : IDisposable
    {
        private ResourceClient resourceClient;
        private ResourceFactoryClient resourceFactoryClient;
        
        public WSTransferClient()
            : this(Constants.ResourceFactoryDefaultConfig, Constants.ResourceClientDefaultConfig)
        {
        }

        public WSTransferClient(String resourceFactoryConfigName, String resourceConfigName)
        {
            resourceClient = new ResourceClient(resourceConfigName);
            resourceFactoryClient = new ResourceFactoryClient(resourceFactoryConfigName);
        }
        /// <summary>
        /// Retrieves a resource management object with the given objectId.
        /// This uses the default WS-Transfer Get behavior which attempts to retrieve an xml serialization of the object.
        /// All attributes that may be retrieves will be returned.
        /// </summary>
        /// <param name="objectId">The object id.  This is an opaque string returned from either enumeration or create.</param>
        /// <returns>A weakly-typed resource management object</returns>
        public ResourceManagementObject Get(String objectId)
        {
            if (String.IsNullOrEmpty(objectId))
                throw new ArgumentException("objectId cannot be null or empty", "objectId");
            GetRequest getRequest = new GetRequest();
            getRequest.ReferenceProperty = new ResourceReferenceProperty();
            getRequest.ReferenceProperty.Value = objectId;

            Message getMessage = Message.CreateMessage(MessageVersion.Default, Constants.WSTransferGetAction, getRequest, new ClientSerializer(typeof(GetRequest)));
            MessageHeader resourceReference = MessageHeader.CreateHeader(Constants.ExtensionResourceReferenceHeaderName,
                Constants.NamespaceResourceManagement,
                getRequest.ReferenceProperty.Value);
            getMessage.Headers.Add(resourceReference);

            Message responseMessage = resourceClient.Get(getMessage);
            StaticSchemaManager.Instance.CheckFault(responseMessage);

            XmlDocument bodyDoc = new XmlDocument();
            bodyDoc.Load(responseMessage.GetReaderAtBodyContents());
            XmlNode resourceManagementObject = bodyDoc.SelectSingleNode("//rm:*", StaticSchemaManager.Instance.ResourceManagementNamespaceManager);
            if (null == resourceManagementObject)
            {
                return null;
            }
            else
            {
                return ResourceManagementObjectFactory.CreateObject(resourceManagementObject);
            }
        }
        /// <summary>
        /// Retrieves a resource management object with the given objectId.
        /// This particular method uses the TEIMO extensions to retrieve particular attributes only.
        /// 
        /// Only attributes in the requestedAttributes list will be included in the returned object, even if you may access more.
        /// </summary>
        /// <param name="objectId">The object id. This is an opaque string returned by enumeration or create</param>
        /// <param name="requestedAttributes">A list of attribute names to specifically retrieve.</param>
        /// <returns>A weakly-typed resource management object.</returns>
        public ResourceManagementObject Get(String objectId, String[] requestedAttributes)
        {
            if (String.IsNullOrEmpty(objectId))
                throw new ArgumentException("objectId cannot be null or empty", "objectId");

            if (null == requestedAttributes)
                return Get(objectId);

            BaseObjectSearchRequest baseObjectSearchRequest = new BaseObjectSearchRequest();
            
            // We don't get object type by default, so it's import to ask for it in order to know what kind of object to create
            bool addObjectType = true;
            bool addObjectId = true;
            foreach (String requestedAttribute in requestedAttributes)
            {
                if (String.IsNullOrEmpty(requestedAttribute))
                    throw new ArgumentException("All requested attributes must be non-null and non-empty", "requestedAttributes");
                if (requestedAttribute.Equals(Constants.AttributeTypeObjectType))
                    addObjectType = false;
                if (requestedAttribute.Equals(Constants.AttributeTypeObjectId))
                    addObjectId = false;
            }
            
            if(addObjectType)
                baseObjectSearchRequest.AttributeTypes.Add(Constants.AttributeTypeObjectType);
            if (addObjectId)
                baseObjectSearchRequest.AttributeTypes.Add(Constants.AttributeTypeObjectId);
            
            baseObjectSearchRequest.AttributeTypes.AddRange(requestedAttributes);
            Message baseSearchObjectRequestMessage = Message.CreateMessage(MessageVersion.Default, Constants.WSTransferGetAction, baseObjectSearchRequest, new ClientSerializer(typeof(BaseObjectSearchRequest)));
            MessageHeader resourceReference = MessageHeader.CreateHeader(Constants.ExtensionResourceReferenceHeaderName,
                Constants.NamespaceResourceManagement,
                objectId);
            baseSearchObjectRequestMessage.Headers.Add(resourceReference);

            // the required header for TEIMO
            MessageHeader identityManagementOperationHeader = MessageHeader.CreateHeader(Constants.ExtensionHeaderName, Constants.NamespaceDirectoryAccess, String.Empty, true);
            baseSearchObjectRequestMessage.Headers.Add(identityManagementOperationHeader);

            #region Temporary
            Console.WriteLine(baseSearchObjectRequestMessage);
            Console.WriteLine();
            #endregion

            Message responseMessage = resourceClient.Get(baseSearchObjectRequestMessage);

            #region Temporary
            Console.WriteLine(responseMessage);
            Console.WriteLine();
            #endregion

            StaticSchemaManager.Instance.CheckFault(responseMessage);

            BaseObjectSearchResponse myResponse = responseMessage.GetBody<BaseObjectSearchResponse>(new ClientSerializer(typeof(BaseObjectSearchResponse)));
            return ResourceManagementObjectFactory.CreateObject(myResponse);
        }

        /// <summary>
        /// Takes an object and generates a put request using TEIMO.  This method only includes changed attributes in the provided resource management object.
        /// </summary>
        /// <param name="putObject">The resource management object to specify with changed attributes.</param>
        public void Put(ResourceManagementObject putObject)
        {
            if(null == putObject)
            {
                throw new ArgumentNullException("putObject", "putObject cannot be null");
            }
            if (putObject[Constants.AttributeTypeObjectId] == null || putObject[Constants.AttributeTypeObjectId].IsMultiValued == true)
            {
                throw new ArgumentException("The provided putObject does not have a single-valued ObjectID attribute", "putObject");
            }

            ModifyRequest modifyRequest = new ModifyRequest();

            // we manually serialize the put request as an XML document
            XmlDocument putDoc = new XmlDocument();

            foreach (String attribName in putObject.AttributeNames)
            {
                if (putObject.IsChanged(attribName))
                {
                    
                    if(putObject[attribName].IsMultiValued)
                    {
                        ReadOnlyCollection<ResourceManagementAttributeChange> changeList = putObject[attribName].AttributeChangeList;

                        foreach (ResourceManagementAttributeChange change in changeList)
                        {
                            modifyRequest.Changes.Add(
                                WSTransferClient.BuildDirectoryAccessChange(
                                    putDoc,
                                    attribName,
                                    change.Operation,
                                    change.Value.ToString()));
                        }
                    }
                    else
                    {
                        modifyRequest.Changes.Add(
                               WSTransferClient.BuildDirectoryAccessChange(
                                putDoc,
                                attribName,
                                Constants.ExtensionIdentityManagementOperationsChangeReplace,
                                putObject[attribName].AttributeSingleValue.ToString())
                           );
                    }
                }
            }

            Message putMessage = Message.CreateMessage(MessageVersion.Default, Constants.WSTransferPutAction, modifyRequest, new ClientSerializer(typeof(ModifyRequest)));
            MessageHeader resourceReference = MessageHeader.CreateHeader(Constants.ExtensionResourceReferenceHeaderName,
                Constants.NamespaceResourceManagement,
                putObject[Constants.AttributeTypeObjectId].AttributeSingleValue.ToString());
            putMessage.Headers.Add(resourceReference);

            // the required header for TEIMO
            MessageHeader identityManagementOperationHeader = MessageHeader.CreateHeader(Constants.ExtensionHeaderName, Constants.NamespaceDirectoryAccess, String.Empty, true);
            putMessage.Headers.Add(identityManagementOperationHeader);

            Message responseMessage = resourceClient.Put(putMessage);
            StaticSchemaManager.Instance.CheckFault(responseMessage);
        }

        /// <summary>
        /// Deletes the object with the given id.
        /// 
        /// Throws an exception upon failure.
        /// </summary>
        /// <param name="objectId">The object id.  This is an opaque string provided by enumeration or create.</param>
        public void Delete(String objectId)
        {
            DeleteRequest deleteRequest = new DeleteRequest();
            deleteRequest.ReferenceProperty = new ResourceReferenceProperty();
            deleteRequest.ReferenceProperty.Value = objectId;

            Message deleteMessage = Message.CreateMessage(MessageVersion.Default, Constants.WSTransferDeleteAction, null, new ClientSerializer(typeof(String)));
            MessageHeader resourceReference = MessageHeader.CreateHeader(Constants.ExtensionResourceReferenceHeaderName,
                Constants.NamespaceResourceManagement,
                deleteRequest.ReferenceProperty.Value);
            deleteMessage.Headers.Add(resourceReference);

            Message responseMessage = resourceClient.Delete(deleteMessage);
            StaticSchemaManager.Instance.CheckFault(responseMessage);
        }

        public String Create(ResourceManagementObject createObject)
        {
            return Create(createObject, true);
        }

        // For RC, we encourage using the extensions to work around known issues.
        // Therefore we are making this interface non-public.
        // We leave the code as a way to show how you can do both calls.
        String Create(ResourceManagementObject createObject, bool useExtensions)
        {
            if (null == createObject)
            {
                throw new ArgumentNullException("createObject", "createObject cannot be null");
            }

            Message createMessage = null;
            if (useExtensions)
            {
                createMessage = CreateMessageUsingExtensions(createObject);
            }
            else
            {
                createMessage = CreateMessageDefault(createObject);
            }

            if (useExtensions)
            {
                MessageHeader identityManagementOperationHeader = MessageHeader.CreateHeader(Constants.ExtensionHeaderName, Constants.NamespaceDirectoryAccess, String.Empty, true);
                createMessage.Headers.Add(identityManagementOperationHeader);
            }

            Message responseMessage = resourceFactoryClient.Create(createMessage);
            StaticSchemaManager.Instance.CheckFault(responseMessage);

            XmlDocument bodyDoc = new XmlDocument();
            bodyDoc.Load(responseMessage.GetReaderAtBodyContents());
            // Please note that the ResourceReferenceProperty element is in the wrong namespace in RC.
            // This is a known issue that we are fixing for RTM.
            XmlNode rmObject = bodyDoc.SelectSingleNode("//wsa:ResourceReferenceProperty", StaticSchemaManager.Instance.ResourceManagementNamespaceManager);
            if (null == rmObject)
            {
                return String.Empty;
            }
            else
            {
                return rmObject.InnerText;
            }
        }

        private static Message CreateMessageDefault(ResourceManagementObject createObject)
        {
            // Please note that it is more reliable to use the extensions when submitting create requests
            // The reason is using raw Xml representations may omit attributes like CreatedTime that are required by schema
            // Since extensions aren't explicitly validated, you may omit those attributes without issue.
            XmlDocument createBody = new XmlDocument();

            XmlElement parentElement = createBody.CreateElement(createObject.ObjectName, Constants.NamespaceResourceManagement);
            createBody.AppendChild(parentElement);
            
            bool addedObjectType = false;

            foreach (String attribName in createObject.AttributeNames)
            {
                if (createObject[attribName].IsMultiValued)
                {
                    ReadOnlyCollection<Object> added = createObject[attribName].AttributeMultiValue;
                    foreach (Object addedObj in added)
                    {
                        parentElement.AppendChild(WSTransferClient.BuildResourceManagementElement(createBody, attribName, addedObj.ToString(), true));
                    }
                }
                else
                {
                    if(attribName.Equals(Constants.AttributeTypeObjectType))
                        addedObjectType = true;
                    parentElement.AppendChild(WSTransferClient.BuildResourceManagementElement(createBody, attribName, createObject[attribName].AttributeSingleValue.ToString(), false));
                }
            }

            if (addedObjectType == false)
                parentElement.AppendChild(WSTransferClient.BuildResourceManagementElement(createBody, Constants.AttributeTypeObjectType, createObject.ObjectName, false));

            return Message.CreateMessage(MessageVersion.Default, Constants.WSTransferCreateAction, createBody, new ClientSerializer(typeof(XmlDocument)));
        }

        private static Message CreateMessageUsingExtensions(ResourceManagementObject createObject)
        {
            AddRequest createRequest = new AddRequest();

            XmlDocument extensionsDoc = new XmlDocument();
            
            bool addedObjectType = false;

            foreach (String attribName in createObject.AttributeNames)
            {
                if (createObject[attribName].IsMultiValued)
                {
                        createRequest.AttributeTypeAndValues.Add(
                           WSTransferClient.BuildDirectoryAccessChange(
                            extensionsDoc, 
                            attribName,
                            null,
                            createObject[attribName].AttributeMultiValue)
                       );
                }
                else
                {
                    if (attribName.Equals(Constants.AttributeTypeObjectType))
                        addedObjectType = true;

                    createRequest.AttributeTypeAndValues.Add(
                        WSTransferClient.BuildDirectoryAccessChange(
                            extensionsDoc,
                            attribName,
                            null,
                            createObject[attribName].AttributeSingleValue)
                    );
                }
            }

            if(addedObjectType == false)
                createRequest.AttributeTypeAndValues.Add(WSTransferClient.BuildDirectoryAccessChange(extensionsDoc, Constants.AttributeTypeObjectType, null, createObject.ObjectName));

            return Message.CreateMessage(MessageVersion.Default, Constants.WSTransferCreateAction, createRequest, new ClientSerializer(typeof(AddRequest)));
        }

        /// <summary>
        /// Provides the network credentials the client uses to connect to the server.
        /// </summary>
        public System.Net.NetworkCredential ClientCredentials
        {
            get
            {
                return this.resourceClient.ClientCredentials.Windows.ClientCredential;
            }
            set
            {
                this.resourceClient.ClientCredentials.Windows.ClientCredential = value;
                this.resourceFactoryClient.ClientCredentials.Windows.ClientCredential = value;
            }
        }

        private static DirectoryAccessChange BuildDirectoryAccessChange(XmlDocument doc, String name, String operation, Object value)
        {
            return BuildDirectoryAccessChange(doc, name, operation, new Object[] { value });
        }

        private static DirectoryAccessChange BuildDirectoryAccessChange(XmlDocument doc, String name, String operation, ICollection<Object> values)
        {
            DirectoryAccessChange newChange = new DirectoryAccessChange();
            newChange.AttributeType = name;
            newChange.Operation = operation;
            foreach (Object value in values)
            {
                XmlNode attributeValue = doc.CreateElement(name, Constants.NamespaceResourceManagement);
                attributeValue.InnerText = value.ToString();
                WSTransferClient.AddInlineSchema(doc, attributeValue, name, values.Count > 1);
                newChange.AttributeValue.Values.Add(attributeValue);
            }
            return newChange;
        }

        private static XmlNode BuildResourceManagementElement(XmlDocument doc, String name, String value, bool isMultiValue)
        {
            XmlNode newNode = doc.CreateElement(name, Constants.NamespaceResourceManagement);
            newNode.InnerText = value;
            WSTransferClient.AddInlineSchema(doc, newNode, name, isMultiValue);
            return newNode;
        }

        /// <summary>
        /// For the release candidate, we require that clients provide the schema of each attribute as XmlAttributes.
        /// This behavior will be removed for the RTM version, and, as such, we are hiding it away in this private method.
        /// We call attention to this in the spirit of openness.
        /// </summary>
        /// <param name="doc"></param>
        /// <param name="currentNode"></param>
        /// <param name="attribName"></param>
        /// <param name="isMultiValue"></param>
        private static void AddInlineSchema(XmlDocument doc, XmlNode currentNode, String attribName, bool isMultiValue)
        {
            XmlAttribute newAttribute = null;
            if (null != currentNode.Attributes["type"])
                newAttribute = currentNode.Attributes["type"];
            else
                newAttribute = doc.CreateAttribute("type", Constants.NamespaceXsdInstance);
            newAttribute.Value = StaticSchemaManager.Instance.GetAttributeType(attribName);
            currentNode.Attributes.Append(newAttribute);

            XmlAttribute maxOccurs = doc.CreateAttribute("maxOccurs", Constants.NamespaceXsd);
            maxOccurs.Value = (isMultiValue ? "unbounded" : "1");
            currentNode.Attributes.Append(maxOccurs);
        }


        #region IDisposable Members

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly")]
        public void Dispose()
        {
            if (null != resourceClient)
            {
                resourceClient.Close();
                resourceClient = null;
            }
            if (null != resourceFactoryClient)
            {
                resourceFactoryClient.Close();
                resourceFactoryClient = null;
            }
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool isManaged)
        {
            if (null != resourceClient)
            {
                resourceClient.Close();
                resourceClient = null;
            }
            if (null != resourceFactoryClient)
            {
                resourceFactoryClient.Close();
                resourceFactoryClient = null;
            }
        }

        #endregion
    }
}
